package pkg.networking;
/**
 * BomberBoy, CPS240 Term Project - Team A
 * Andrew Osterink, David Bash, Scott Kidder
 *	This class holds common functionality of the client and server.
 *   This includes serverClient Message pump and default implementations
 *   for writing/reading packets (RemoteCommands).
 */

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.*;
import java.util.*;

abstract class NetworkConnection implements Runnable  {

	private final int FIXEDTHREADCOUNT = 4;
	
	protected Selector selector;
	protected boolean connected = false;
	protected Logger log;
	private HashMap<SocketChannel,Queue<ByteBuffer>> writeRequests;
	private HashMap<SocketChannel,ByteBuffer> currentRead;
	private ByteBuffer curPacketRead;
	protected ExecutorService threadPool;
	protected SocketChannel test;
	
	public NetworkConnection()
	{
		writeRequests = new HashMap<SocketChannel,Queue<ByteBuffer>>();
		currentRead = new HashMap<SocketChannel,ByteBuffer>();
		threadPool = Executors.newFixedThreadPool(FIXEDTHREADCOUNT);
	}
	
	/**
	 * This function accepts in coming and outgoing connections
	 * and data. Once an event is found it is sent to an event method
	 * so that a sub class can handle what happens with the action.
	 */
	public void run() 
	{
	 	while(connected)
	 	{
	 		try 
	 		{
				this.selector.select();
				
				for(SelectionKey key : selector.selectedKeys())
				{
					if(!key.isValid())
							continue;
						if(key.isAcceptable())
							onAcceptConnection(key);
						else if(key.isConnectable())
							onConnect(key);
						else if(key.isReadable())
							onRead(key); 
						else if(key.isWritable())
					    	onWrite(key);
					}
	 			} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
	 			catch(Exception e) {
	 				e.printStackTrace();
	 			}
	 		}
		log.log(Level.FINEST,"Network Pump Finished");
	 }
	
 /**
  * Starts the tread for message pump
  */
 protected void startPump()
 {
	 Thread t = new Thread(this);
	 t.start();
 }
	
 /** 
  * Called when a connection is accepted into the server
  * @param key key associated with action
  **/
 protected void onAcceptConnection(SelectionKey key){}
 
 /** Actualy not sure when this is called.... acording to documentation its called 
  * so the client can call finishConnection() on the client but iv never had this happen
  * @param key associated with action
  **/
 protected void onConnect(SelectionKey key){}
 
 /**
  * Called whenever data is retreived from a client or server
  * @param key key associatedd with client
  */
 protected void onRead(SelectionKey key,ByteBuffer data){}
 
 private void onRead(SelectionKey key)
 {
	 SocketChannel socketChannel = (SocketChannel) key.channel();
	 
	 try {

		 if(currentRead.get(socketChannel) == null) //newPacket find the size and allocate buffer
		 {
			 test = socketChannel;
			 int size = -1;
			 ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
			 socketChannel.read(sizeBuffer);
			 sizeBuffer.rewind();
			 size = sizeBuffer.getInt(); //read packet size header
			 if(size == 0)
				 return;
			 log.finest("Reading Packet Size: " + size);
			 
			 curPacketRead = ByteBuffer.allocate(size);
			 currentRead.put(socketChannel,curPacketRead);
		 }
		 else
		 {
			 curPacketRead = currentRead.get(socketChannel);
		 }
		 
		 socketChannel.read(curPacketRead); //read as much as we can
		 if(curPacketRead.remaining() <= 0) //packet is compleatly read notify subclasses
		 {
			 curPacketRead.rewind();
			 onRead(key,curPacketRead);
			 curPacketRead = null; //set to null so we can read new packet
			 currentRead.put(socketChannel, null);
		 } //else return and finish reading packet later

	 }
	 catch(IOException ex) {
		 disconectKey(key);
	 }
	 
 }
 
 /**
  *  Called when the server is ready to write this should only be called by the pump.
  *  This event can be invoked by setting a keys interest ops. but data transfer to the
  *  event must be handled by subclass that wants to invoke command.
  * @param key key associated with action
  */
 private void onWrite(SelectionKey key)
 {
	 
	 synchronized(writeRequests) 
  	 {
	 SocketChannel socketChannel = (SocketChannel) key.channel();
	 Queue<ByteBuffer> data = writeRequests.get(socketChannel);
	 ByteBuffer dataBuffer = null;
	 
	 if((data == null || data.peek() == null) && data.size() <= 0)
	 {
		 key.interestOps(SelectionKey.OP_READ);
		 return;
	 }
	 else 
	    dataBuffer = data.peek();
	  
	 try {
		
		socketChannel.write(dataBuffer);
	} catch (IOException e) {
		log.severe("Failure to write bytes to channel");
		log.warning(e.toString());
	}
	//if we are done writing this data discard it else save it so we
	//can try to write it again
	if(dataBuffer.remaining() <= 0)
		data.poll();
	
	if(data.isEmpty())
		key.interestOps(SelectionKey.OP_READ);
  	 }
 }

 /**
  * Utility function to write packet to a  key
  */
 protected void writePacket(SelectionKey key,RemoteCommand packet)
 {
		byte[] messageBytes = serializeCommand(packet).toByteArray();
		ByteBuffer dataBuffer = ByteBuffer.wrap(messageBytes);
		writeBytes(key,dataBuffer);
 }
 
 protected void writeBytes(SelectionKey key,ByteBuffer dataBuffer)
 {
	 if(key == null)
		 System.exit(0);
	 dataBuffer.rewind();
	 SocketChannel channel = (SocketChannel)key.channel();
	 Queue<ByteBuffer> data = writeRequests.get(channel);
	 //make room for size to be sent first
	 ByteBuffer newBuffer = ByteBuffer.allocate(dataBuffer.capacity() + 4);
	 log.finest("Writeing Size: " + dataBuffer.capacity());
	 newBuffer.putInt(dataBuffer.capacity());
	 newBuffer.put(dataBuffer);
	 newBuffer.rewind();
	 synchronized(writeRequests) 
  	 {
		if(data == null)
		{
			data = new PriorityQueue<ByteBuffer>();
			writeRequests.put(channel, data);
		}
		data.add(newBuffer);
		key.interestOps(SelectionKey.OP_WRITE | SelectionKey.OP_READ);
		selector.wakeup();
	 }
 }
 
 protected void writeBytes(SocketChannel channel,ByteBuffer dataBuffer)
 {
	writeBytes(channel.keyFor(selector),dataBuffer); 
 }

 
 protected void onDisconect(SelectionKey key) {}
 
 protected void disconectKey(SelectionKey key)
 {
	 try{
		key.cancel();
		key.channel().close();
		onDisconect(key);
	} catch (IOException e) {
		e.printStackTrace();
	}
 }
 
 protected ByteArrayOutputStream serializeCommand(RemoteCommand packet)
{
		ByteArrayOutputStream bytes = new ByteArrayOutputStream();
		ObjectOutputStream stream = null;
		try {
			 stream = new ObjectOutputStream(bytes);
			 stream.writeObject(packet);
		} catch (IOException e) {
			log.severe("Error Serializing Remote Command");
			log.info(e.toString());
		}
		
		return bytes;
}
 
 protected RemoteCommand deserializeCommand(ByteBuffer buffer) throws IOException
 {
	 RemoteCommand packet = null;
	 try
	 {
		 ByteArrayInputStream bytes = new ByteArrayInputStream(buffer.array());
		 ObjectInputStream stream = new ObjectInputStream(bytes);
		 packet = (RemoteCommand)stream.readObject();
	 }catch (ClassNotFoundException ex) { ex.printStackTrace();}
	return packet;
 }

}